Laravel 路由

路由构造总览

构造方法有:
Route::get、Route::post、Route::put、Route::patch、Route::delete、Route::options、Route::any、Route::match、Route::resource、Route::resources、Route::group


Route::get('foo', function () {
    // 基本方式
});
Route::match(['get', 'post'], '/', function () {
    // 基本方式
});
Route::any('foo', function () {
    // 基本方式
});

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    // 必选路由参数
});

Route::get('user/{name?}', function ($name = 'John') {
    // 可选路由参数
});

Route::get('user/{id}/{name}', function ($id, $name) {
    // 正则表达式约束
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

// 全局约束 RouteServiceProvider 的 boot 方法
public function boot()
{
    Route::pattern('id', '[0-9]+'); 
    parent::boot();
}
Route::get('user/{id}', function ($id) {
    // 仅在 {id} 为数字时执行...
});

Route::get('user/profile', function () {
    // 命名路由
})->name('profile');
Route::get('user/profile', 'UserController@showProfile')->name('profile');
为命名路由生成:
// 生成 URL...
$url = route('profile');
// 生成重定向...
return redirect()->route('profile');

// 路由组
Route::group(['middleware' => 'auth'], function () {
    Route::get('/', function ()    {
        // 使用 `Auth` 中间件
    });

    Route::get('user/profile', function () {
        // 使用 `Auth` 中间件
    });
});

命名空间|子域名路由|路由前缀
Route::group(['namespace' => 'Admin','domain' => '{account}.myapp.com','prefix' => 'admin'], function () {
    // 在 "App\Http\Controllers\Admin" 命名空间下,子域名为{account}.myapp.com,路由前缀匹配 '/admin' 的控制器
});

Route::resource('photo', 'PhotoController', ['except' => ['create', 'store', 'update', 'destroy'], 'names' => ['create' => 'photo.build'],'middleware' => []);

路由模型绑定
隐式绑定#
Laravel 会自动解析定义在路由或控制器方法(方法包含和路由片段匹配的已声明类型变量)中的 Eloquent 模型
Route::get('api/users/{user}', function (App\User $user) {
    return $user->email;
});
显式绑定
RouteServiceProvider 类中的 boot 方法
public function boot()
{
    parent::boot();    
    Route::model('user', App\User::class);
}
Route::get('profile/{user}', function (App\User $user) {
    //
});

自定义解析逻辑
public function boot()
{
    parent::boot();    
    Route::bind('user', function ($value) {
        return App\User::where('name', $value)->first();
    });
}

基本有以下几种形式:uri 分为是否带有参数, action 分为匿名函数或者 Controller@Method 形式,可能还会带一些其他的前置操作

基本构造

Route::get、Route::post、Route::put、Route::patch、Route::delete、Route::options、Route::any、Route::match

以上的构造方法本质是一样的,区别在于第一个参数

public function get($uri, $action = null)
{
    return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
protected function addRoute($methods, $uri, $action)
{
    // 创建 $route(\Illuminate\Routing\Route) 对象并加入到集合(\Illuminate\Routing\RouteCollection 路由集合辅助类)里,再返回 $route
    return $this->routes->add($this->createRoute($methods, $uri, $action));
}
protected function createRoute($methods, $uri, $action)
{
    // $action 若为 Controller@Method|['uses'=>Controller@Method] 形式
    if ($this->actionReferencesController($action)) {
        $action = $this->convertToControllerAction($action);
    }

    $route = $this->newRoute(
        $methods, $this->prefix($uri), $action
    );
    // 如果前缀条件栈不为空,则对 $route 进行相应的设置
    if ($this->hasGroupStack()) {
        $this->mergeGroupAttributesIntoRoute($route);
    }
    // 将 where 前置条件注入到 $route 对象
    $this->addWhereClausesToRoute($route);

    return $route;
}
protected function actionReferencesController($action)
{
    if (! $action instanceof Closure) {
        return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
    }

    return false;
}
protected function convertToControllerAction($action)
{
    if (is_string($action)) {
        $action = ['uses' => $action];
    }
    // 尝试加入前置条件 namespace
    if (! empty($this->groupStack)) {
        $action['uses'] = $this->prependGroupNamespace($action['uses']);
    }
    // 通过控制器来获取 action
    $action['controller'] = $action['uses'];
    // 类似:['controller'=>'namespace\Controller@Method', 'uses'=>'namespace\Controller@Method']
    return $action;
}
// $uri 尝试增加前置条件 prefix(group 组中的 prefix,对应给下面的所有路由增加)
protected function prefix($uri)
{
    return trim(trim($this->getLastGroupPrefix(), '/').'/'.trim($uri, '/'), '/') ?: '/';
}
public function getLastGroupPrefix()
{
    if (! empty($this->groupStack)) {
        $last = end($this->groupStack);

        return isset($last['prefix']) ? $last['prefix'] : '';
    }

    return '';
}
protected function newRoute($methods, $uri, $action)
{
    return (new Route($methods, $uri, $action))
                ->setRouter($this)
                ->setContainer($this->container);
}
// new Route
public function __construct($methods, $uri, $action)
{
    $this->uri = $uri;
    $this->methods = (array) $methods;
    $this->action = $this->parseAction($action);

    if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
        $this->methods[] = 'HEAD';
    }
    // 再尝试给 uri 单独的加入 prefix 前缀 
    if (isset($this->action['prefix'])) {
        $this->prefix($this->action['prefix']);
    }
}
protected function parseAction($action)
{
    // 委托 RouteAction action 辅助类进行解析
    return RouteAction::parse($this->uri, $action);
}
public static function parse($uri, $action)
{
    if (is_null($action)) {
        return static::missingAction($uri); // 抛异常
    }
    // 匿名函数
    if (is_callable($action)) {
        return ['uses' => $action];
    }
    elseif (! isset($action['uses'])) {
        $action['uses'] = static::findCallable($action);
    }
    // 如果 $action['uses'] 类似 Controller 形式,则尝试构造为 Controller@__invoke 形式,即没有指定方法时调用 __invoke 方法
    if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
        $action['uses'] = static::makeInvokable($action['uses']);
    }

    return $action;
}
 protected static function findCallable(array $action)
{
    // 尝试从 $action 数组找到第一个满足可调用且为数字键的值作为 $action 返回
    return Arr::first($action, function ($value, $key) {
        return is_callable($value) && is_numeric($key);
    });
}
public function hasGroupStack()
{
    return ! empty($this->groupStack);
}
protected function mergeGroupAttributesIntoRoute($route)
{
    $route->setAction($this->mergeWithLastGroup($route->getAction()));
}
public function mergeWithLastGroup($new)
{
    // 使用上一层的 groupStack 设置
    return RouteGroup::merge($new, end($this->groupStack));
}
protected function addWhereClausesToRoute($route)
{
    $route->where(array_merge(
        $this->patterns, isset($route->getAction()['where']) ? $route->getAction()['where'] : []
    ));

    return $route;
}
// 返回 \Illuminate\Routing\Route 对象
public function add(Route $route)
{
    // 设置路由以何种方式放入路由集合,待后续按此种方式来获取
    $this->addToCollections($route);
    $this->addLookups($route);
    return $route;
}
protected function addToCollections($route)
{
    $domainAndUri = $route->domain().$route->uri();
    // 表面可以通过 method 和 uri 来获取路由
    foreach ($route->methods() as $method) {
        $this->routes[$method][$domainAndUri] = $route;
    }    
    $this->allRoutes[$method.$domainAndUri] = $route;
}
protected function addLookups($route)
{
    $action = $route->getAction();
    // 如果前置条件栈设置了 as ,则将 $route 注入到 $this->nameList,即可以通过名字来获取路由
    if (isset($action['as'])) {
        $this->nameList[$action['as']] = $route;
    }        
    if (isset($action['controller'])) {
        $this->addToActionList($action, $route);
    }
}
protected function addToActionList($action, $route)
{
    // 表示可以通过控制器获取路由
    $this->actionList[trim($action['controller'], '\\')] = $route;
}

流程小结(创建 route ,并将加入到路由集合里进行统一的管理)

  1. 根据 action 的形式和前置条件,或转为数组(['use'=> Clause|namespaceController@Method]),或为匿名函数

  2. 根据前置条件,或将组 uri 加前缀

  3. 创建 route 对象,并将 action 统一为数组,再进行一些其他设置

  4. 若存在前置条件,则加入到 route 对象的 action 数组

  5. route 对象加入 where 条件

其他构造

Route::group

public function group(array $attributes, $routes)
{
    $this->updateGroupStack($attributes);

    $this->loadRoutes($routes);

    array_pop($this->groupStack);
}
protected function updateGroupStack(array $attributes)
{
    if (! empty($this->groupStack)) {
        $attributes = RouteGroup::merge($attributes, end($this->groupStack));
    }

    $this->groupStack[] = $attributes;
}
protected function loadRoutes($routes)
{
    if ($routes instanceof Closure) {
        $routes($this);     // 注意:每个匿名函数都会有 router 对象
    } else {
        $router = $this;

        require $routes;
    }
}

小结

主要通过设置前置条件栈($groupStack),然后运用到组内的所有成员,本质还是基本构造

Route::resource、Route::resources

public function resource($name, $controller, array $options = [])
{
    if ($this->container && $this->container->bound(ResourceRegistrar::class)) {
        $registrar = $this->container->make(ResourceRegistrar::class);
    } else {
        $registrar = new ResourceRegistrar($this);
    }

    $registrar->register($name, $controller, $options);
}
public function __construct(Router $router)
{
    $this->router = $router;
}
public function register($name, $controller, array $options = [])
{
    if (isset($options['parameters']) && ! isset($this->parameters)) {
        $this->parameters = $options['parameters'];
    }

    if (Str::contains($name, '/')) {
        $this->prefixedResource($name, $controller, $options);

        return;
    }

    $base = $this->getResourceWildcard(last(explode('.', $name)));
    // ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']
    $defaults = $this->resourceDefaults;
    // 生成相应条件下的路由
    foreach ($this->getResourceMethods($defaults, $options) as $m) {
        $this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
    }
}
protected function prefixedResource($name, $controller, array $options)
{
    list($name, $prefix) = $this->getResourcePrefix($name);
    // $me 为 router 对象。本质是将 $name 为 'xx/yy/zz' 的 resource 请求转化为 groupStack 追加 ['prefix'=>'xx/yy'] 的 group 组内请求,对应的匿名函数依然是 $name 为 'zz' 的 resource 请求
    $callback = function ($me) use ($name, $controller, $options) {
        $me->resource($name, $controller, $options);
    };

    return $this->router->group(compact('prefix'), $callback);
}
protected function getResourcePrefix($name)
{
    $segments = explode('/', $name);

    $prefix = implode('/', array_slice($segments, 0, -1));
    // 假如 $name 为 'xx/yy/zz', 则返回 ['zz', 'xx/yy']
    return [end($segments), $prefix];
}
// 优先从设置里面取值,没有则生成单数形式的字符串,并将字符 '-' 替换为 '_'
public function getResourceWildcard($value)
{
    if (isset($this->parameters[$value])) {
        $value = $this->parameters[$value];
    } elseif (isset(static::$parameterMap[$value])) {
        $value = static::$parameterMap[$value];
    } elseif ($this->parameters === 'singular' || static::$singularParameters) {
        $value = Str::singular($value);
    }

    return str_replace('-', '_', $value);
}
protected function getResourceMethods($defaults, $options)
{
    if (isset($options['only'])) {
        return array_intersect($defaults, (array) $options['only']);
    } elseif (isset($options['except'])) {
        return array_diff($defaults, (array) $options['except']);
    }

    return $defaults;
}
protected function addResourceIndex($name, $base, $controller, $options)
{
    $uri = $this->getResourceUri($name);

    $action = $this->getResourceAction($name, $controller, 'index', $options);

    return $this->router->get($uri, $action);
}
public function getResourceUri($resource)
{
    if (! Str::contains($resource, '.')) {
        return $resource;
    }

    $segments = explode('.', $resource);

    $uri = $this->getNestedResourceUri($segments);
    // 'xx/{xx}/yy/{yy}/zz'
    return str_replace('/{'.$this->getResourceWildcard(end($segments)).'}', '', $uri);
}
protected function getNestedResourceUri(array $segments)
{
    // ['xx','yy','zz'] => 'xx/{xx}/yy/{yy}/zz/{zz}'
    return implode('/', array_map(function ($s) {
        return $s.'/{'.$this->getResourceWildcard($s).'}';
    }, $segments));
}
protected function getResourceAction($resource, $controller, $method, $options)
{
    $name = $this->getResourceRouteName($resource, $method, $options);

    $action = ['as' => $name, 'uses' => $controller.'@'.$method];

    if (isset($options['middleware'])) {
        $action['middleware'] = $options['middleware'];
    }

    return $action;
}
protected function getResourceRouteName($resource, $method, $options)
{
    $name = $resource;
    
    if (isset($options['names'])) {
        if (is_string($options['names'])) {
            $name = $options['names'];
        } elseif (isset($options['names'][$method])) {
            return $options['names'][$method];
        }
    }

    $prefix = isset($options['as']) ? $options['as'].'.' : '';

    return trim(sprintf('%s%s.%s', $prefix, $name, $method), '.');
}

小结

资源类型的构造,实际上会被转化为构造多个默认资源的路由,本质依然是基本构造


TylerZou
70 声望20 粉丝

I have a dream!